﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BFlatCompile.Serializer
{
	public class Buffer
	{
		private List<Cell> cells;
		private List<Cell> breakStatements;
		private List<int> breakLocations;
		private List<Token[]> tokensByCell;

		public Buffer()
		{
			this.cells = new List<Cell>();
			this.breakStatements = new List<Cell>();
			this.breakLocations = new List<int>();
			this.tokensByCell = new List<Token[]>();
		}

		public void ResolveTryPushOffsetsIntoAbsoluteAddresses(Compiler compiler)
		{
			if (compiler.IsReadable)
			{
				// Don't bother in readable mode.
				// Offsets are equally useful in debugging.
				return;
			}

			// Total hack, but I didn't feel inclined to make ByteCodes.CODE_TRY_PUSH internal from private
			string tryPushOpCode = ByteCodes.TRY_PUSH(compiler, 0, 0)[0] + "";

			int length = this.cells.Count;
			Cell cell;
			for (int pc = 0; pc < length; ++pc)
			{
				cell = this.cells[pc];
				if (cell.Value.StartsWith(tryPushOpCode))
				{
					string[] offsets = cell.Value.Substring(1, cell.Value.Length - 2).Split(','); // trim off the op code and the trailing |
					if (offsets.Length == 2)
					{
						int catchOffset = Util.FromHex(offsets[0]);
						int finallyOffset = Util.FromHex(offsets[1]);

						int catchLocation = catchOffset == 0 ? 0 : catchOffset + pc;
						int finallyLocation = finallyOffset == 0 ? 0 : finallyOffset + pc;

						cell.DontUseThisFunctionUnlessYouKnowTheConsequences(
							ByteCodes.TRY_PUSH(compiler, catchLocation, finallyLocation));
					}
					else
					{
						throw new Exception("TRY_PUSH did not have 2 args.");
					}
				}
			}
		}

		// All unresolved breaks will be pointed to the end of the current buffer
		public void ResolveBreaksAndContinues(Compiler c, bool continuesAllowed)
		{
			int size = this.Size;
			int breaks = this.breakStatements.Count;
			for (int i = 0; i < breaks; ++i)
			{
				Cell cell = this.breakStatements[i];
				if (!continuesAllowed && cell.IsContinue)
				{
					// TODO: verify that continues cannot be used in a switch statement outside of a loop.
					throw new Exception("continue statements are not allowed here.");
				}
				int location = this.breakLocations[i];
				int offset = cell.IsBreak ? (size - location - 1) : location;
				cell.ResolveJump(c, offset);
			}

			this.breakStatements.Clear();
			this.breakLocations.Clear();
		}

		private static readonly Token[] EMPTY_TOKEN_LIST = new Token[0];

		public void Add(string cellValue)
		{
			this.Add(cellValue, EMPTY_TOKEN_LIST);
		}

		public void Add(string cellValue, Token relevantToken)
		{
			this.Add(cellValue, relevantToken == null ? EMPTY_TOKEN_LIST : new Token[] { relevantToken });
		}

		public void Add(string cellValue, Token[] relevantTokens)
		{
			if (cellValue == null)
			{
				throw new Exception("Should use AddBreak to add break statements");
			}
			Cell cell = new Cell(cellValue);
			this.cells.Add(cell);
			this.tokensByCell.Add(relevantTokens);
		}

		public void AddBreak()
		{
			Cell b = Cell.CreateBreak();
			this.cells.Add(b);
			this.tokensByCell.Add(null);
			this.breakStatements.Add(b);
			this.breakLocations.Add(this.cells.Count - 1);
		}

		public void AddContinue()
		{
			Cell c = Cell.CreateContinue();
			this.cells.Add(c);
			this.tokensByCell.Add(null);
			this.breakStatements.Add(c);
			this.breakLocations.Add(this.cells.Count - 1);
		}

		public int Size { get { return this.cells.Count; } }

		public void Add(Buffer buffer)
		{
			foreach (int loc in buffer.breakLocations)
			{
				this.breakLocations.Add(loc + this.Size);
			}

			this.cells.AddRange(buffer.cells);
			this.tokensByCell.AddRange(buffer.tokensByCell);
			this.breakStatements.AddRange(buffer.breakStatements);
		}

		public List<Cell> Cells { get { return this.cells; } }

		public List<Token[]> Tokens { get { return this.tokensByCell; } }

		public string GenerateStringValue(Compiler compiler)
		{
			StringBuilder b = new StringBuilder();
			Cell cell;
			for (int i = 0; i < this.cells.Count; ++i)
			{
				cell = this.cells[i];
#if DEBUG
				if (cell.Value == null)
				{
					throw new Exception("Break/Continue was not resolved.");
				}
#endif
				if (compiler.IsReadable)
				{
					b.Append(i + ": " + cell.Value + "\n");
				}
				else
				{
					b.Append(cell.Value);
				}
			}
			return b.ToString();
		}

		public string GenerateTokenValues(Compiler compiler, Dictionary<string, int> fileIndices)
		{
			if (compiler.IsReadable)
			{
				return "TODO: Readable token information";
			}

			StringBuilder b = new StringBuilder();
			string rowDelimit = Util.GetRowDelimiter(compiler);
			string colDelimit = Util.GetColDelimiter(compiler);
			Token[] tokens;
			Token token;
			int fileId;
			for (int i = 0; i < this.tokensByCell.Count; ++i)
			{
				if (i > 0)
				{
					b.Append(rowDelimit);
				}

				tokens = this.tokensByCell[i];
				if (tokens == null || tokens.Length == 0)
				{
					// File ID of 0 indicates it's a built-in file.
					// Line/Col information is worthless here.
					// In these instances, an empty token list ought to be used
					//b.Append("0");
				}
				else
				{
					for (int j = 0; j < tokens.Length; ++j)
					{
						if (j > 0)
						{
							b.Append(colDelimit);
						}

						token = tokens[j];
						if (fileIndices.TryGetValue(token.File, out fileId))
						{
							b.Append(Util.ToHex(compiler, fileId));
							b.Append(colDelimit);
							b.Append(Util.ToHex(compiler, token.LineNum));
							b.Append(colDelimit);
							b.Append(Util.ToHex(compiler, token.Column));
						}
					}
				}
			}

			return b.ToString();
		}
	}
}
